/** * @brief x util is the utility library which includes the method extentions for common data types * * @author Callum Taylor **/ package in.lib.utils; import java.io.ByteArrayOutputStream; import java.io.FileDescriptor; import android.graphics.Bitmap; import android.graphics.Bitmap.CompressFormat; import android.graphics.Bitmap.Config; import android.graphics.BitmapFactory; import android.graphics.BitmapFactory.Options; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.Rect; import android.media.ExifInterface; /** * @brief Utilities for bitmap processing and manipulation */ public class BitmapUtils { public static int FLIP_HORIZONTAL = 0x01; public static int FLIP_VERTICAL = 0x10; // Orientation vars public static final int ORIENTATION_HORIZONTAL = 2; public static final int ORIENTATION_180_ROTATE_LEFT = 3; public static final int ORIENTATION_VERTICAL_FLIP = 4; public static final int ORIENTATION_VERTICAL_FLIP_90_ROTATE_RIGHT = 5; public static final int ORIENTATION_90_ROTATE_RIGHT = 6; public static final int ORIENTATION_HORIZONTAL_FLIP_90_ROTATE_RIGHT = 7; public static final int ORIENTATION_90_ROTATE_LEFT = 8; private static boolean mRecycleBitmaps = true; private static Config mBitmapConfig = Config.ARGB_8888; /** * Set if the utils library recycles the bitmaps after processing * @param recycleBitmaps */ public static void setRecycleBitmaps(boolean recycleBitmaps) { mRecycleBitmaps = recycleBitmaps; } public static void setConfig(Config config) { mBitmapConfig = config; } /** * Recursivly samples an image to below or equal the max width/height * @param path The path to the image * @param maxWidth The maximum width the image can be * @param maxHeight The maximum height the image can be * @return The scale size of the image to use with {@link android.graphics.BitmapFactory.Options} */ public static int recursiveSample(String path, int maxWidth, int maxHeight) { Options options = new Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeFile(path, options); int scale = 1; int imageWidth = options.outWidth; int imageHeight = options.outHeight; while (imageWidth / 2 >= maxWidth || imageHeight / 2 >= maxHeight) { imageWidth /= 2; imageHeight /= 2; scale *= 2; } if (scale < 1) { scale = 1; } return scale; } /** * Recursivly samples an image to below or equal the max width/height * @param fd The path to the image * @param maxWidth The maximum width the image can be * @param maxHeight The maximum height the image can be * @return The scale size of the image to use with {@link android.graphics.BitmapFactory.Options} */ public static int recursiveSample(FileDescriptor fd, int maxWidth, int maxHeight) { Options options = new Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeFileDescriptor(fd, null, options); int scale = 1; int imageWidth = options.outWidth; int imageHeight = options.outHeight; while (imageWidth / 2 >= maxWidth || imageHeight / 2 >= maxHeight) { imageWidth /= 2; imageHeight /= 2; scale *= 2; } if (scale < 1) { scale = 1; } return scale; } /** * Makes sure the colour does not exceed the 0-255 bounds * @param colour The colour integer * @return The numerical value of the colour */ public static int safe(int colour) { return Math.min(255, Math.max(colour, 0)); } /** * Makes sure the colour does not exceed the 0-255 bounds * @param colour The colour integer * @return The numerical value of the colour */ public static int safe(double colour) { return (int)Math.min(255.0, Math.max(colour, 0.0)); } /** * Duplicates a bitmap. This does <b>not</b> recycle the original bitmap after the method is called * @param bm The bitmap you wish to duplicate * @return The new bitmap */ public static Bitmap duplicate(Bitmap bm) { Bitmap newBitmap = Bitmap.createBitmap(bm); return newBitmap; } /** * Resizes a bitmap. Original bitmap is recycled after this method is called. * @param bm The bitmap to resize * @param width The new width * @param height Thew new height * @return The resized bitmap */ public static Bitmap resize(Bitmap bm, int width, int height) { Bitmap newBitmap = Bitmap.createBitmap(width, height, mBitmapConfig); Canvas canvas = new Canvas(newBitmap); canvas.drawBitmap(bm, new Rect(0, 0, bm.getWidth(), bm.getHeight()), new Rect(0, 0, width, height), new Paint(Paint.ANTI_ALIAS_FLAG)); if (mRecycleBitmaps) { bm.recycle(); } return newBitmap; } /** * Resizes a bitmap to a specific width, maintaining the ratio. Original bitmap is recycled after this method is called. * @param bm The bitmap to resize * @param width The new width * @return The resized bitmap */ public static Bitmap resizeToWidth(Bitmap bm, int width) { // Calculate the new height float ratio = (float)width / (float)bm.getWidth(); int height = (int)(bm.getHeight() * ratio); return BitmapUtils.resize(bm, width, height); } /** * Resizes a bitmap to a specific height, maintaining the ratio. Original bitmap is recycled after this method is called. * @param bm The bitmap to resize * @param height The new width * @return The resized bitmap */ public static Bitmap resizeToHeight(Bitmap bm, int height) { // Calculate the new width float ratio = (float)height / (float)bm.getHeight(); int width = (int)(bm.getWidth() * ratio); return BitmapUtils.resize(bm, width, height); } /** * Resizes a bitmap to a which ever axis is the largest, maintaining the ratio. Original bitmap is recycled after this method is called. * @param bm The bitmap to resize * @param maxWidth The max width size * @param maxHeight The max height size * @return The resized bitmap */ public static Bitmap maxResize(Bitmap bm, int maxWidth, int maxHeight) { // Calculate what is larger width or height if (bm.getWidth() > bm.getHeight()) { return BitmapUtils.resizeToWidth(bm, maxWidth); } else { return BitmapUtils.resizeToHeight(bm, maxHeight); } } /** * Compresses a bitmap. original bitmap is recycled after this method is called. * @param bm The bitmap to be compressed. * @param compression The compression ratio 0-100. * @return The compressed bitmap. */ public static Bitmap compress(Bitmap bm, int compression) { ByteArrayOutputStream bitmapOutputStream = new ByteArrayOutputStream(); bm.compress(CompressFormat.JPEG, compression, bitmapOutputStream); if (mRecycleBitmaps) { bm.recycle(); } return BitmapFactory.decodeByteArray(bitmapOutputStream.toByteArray(), 0, bitmapOutputStream.size()); } /** * Rotates a bitmap. Original bitmap is recycled after this method is called. * @param bm The bitmap to rotate * @param degrees The degrees to rotate at. 0-360 clockwise. * @return The rotated bitmap */ public static Bitmap rotate(Bitmap bm, int degrees) { Matrix rotateMatrix = new Matrix(); rotateMatrix.setRotate(degrees); Bitmap newBitmap = Bitmap.createBitmap(bm, 0, 0, bm.getWidth(), bm.getHeight(), rotateMatrix, true); if (mRecycleBitmaps) { bm.recycle(); } return newBitmap; } /** * Flips an image * @param bm The image to flip. Original bitmap is recycled after this method is called. * @param mode The mode to flip * @return The flipped bitmap */ public static Bitmap flip(Bitmap bm, int mode) { Bitmap newBitmap = Bitmap.createBitmap(bm.getWidth(), bm.getHeight(), mBitmapConfig); Canvas canvas = new Canvas(newBitmap); Matrix flipMatrix = new Matrix(); float xFlip = 1.0f, yFlip = 1.0f; if ((mode & FLIP_HORIZONTAL) == mode) { xFlip = -1.0f; } if ((mode & FLIP_VERTICAL) == mode) { yFlip = -1.0f; } flipMatrix.preScale(xFlip, yFlip); canvas.drawBitmap(bm, flipMatrix, new Paint()); if (mRecycleBitmaps) { bm.recycle(); } return newBitmap; } /** * Crops a bitmap at the given indexes. Original bitmap is recycled after this method is called. * @param bm The bitmap to crop * @param startX The start x coord starting in TOP LEFT * @param startY The start y coord starting in TOP LEFT * @param width The width of the crop * @param height The height of the crop * @return The newly cropped bitmap. */ public static Bitmap crop(Bitmap bm, int startX, int startY, int width, int height) { int w = width; int h = height; Bitmap ret = Bitmap.createBitmap(w, h, Config.ARGB_8888);//bm.getConfig()); Canvas canvas = new Canvas(ret); canvas.drawBitmap(bm, -startX, -startY, null); if (mRecycleBitmaps) { bm.recycle(); } return ret; } /** * Blend modes to use with the merge method */ public static enum BlendMode { // Standard PorterDuff modes CLEAR, DARKEN, DST, DST_ATOP, DST_IN, DST_OUT, DST_OVER, LIGHTEN, MULTIPLY, SCREEN, SRC, SRC_ATOP, SRC_IN, SRC_OUT, SRC_OVER, XOR, // Custom Blend modes NORMAL, OVERLAY, //ADD, DIFFERENCE, //EXCLUSION, SOFTLIGHT } /** * Merges an image ontop of another image using a specific blend mode. Both images are recycled after the merge. * @param original The bottom image * @param overlay The image to merge to * @param blendMode The blending mode of the merge * @return The merged images */ public static Bitmap merge(Bitmap original, Bitmap overlay, BlendMode blendMode) { try { PorterDuff.Mode m = PorterDuff.Mode.valueOf(blendMode.name()); int w = original.getWidth(); int h = original.getHeight(); Bitmap newBitmap = Bitmap.createBitmap(w, h, mBitmapConfig); Canvas c = new Canvas(newBitmap); c.drawBitmap(original, 0, 0, null); Paint paint = new Paint(); paint.setXfermode(new PorterDuffXfermode(m)); c.drawBitmap(overlay, new Rect(0, 0, overlay.getWidth(), overlay.getHeight()), new Rect(0, 0, w, h), paint); if (mRecycleBitmaps) { original.recycle(); overlay.recycle(); } return newBitmap; } catch (IllegalArgumentException e) { int w = original.getWidth(); int h = original.getHeight(); Bitmap newBitmap = Bitmap.createBitmap(w, h, mBitmapConfig); Canvas c = new Canvas(newBitmap); //c.drawBitmap(overlay, new Rect(0, 0, overlay.getWidth(), overlay.getHeight()), new Rect(0, 0, w, h), new Paint()); if (blendMode == BlendMode.NORMAL) { c.drawBitmap(original, 0, 0, new Paint()); c.drawBitmap(overlay, new Rect(0, 0, overlay.getWidth(), overlay.getHeight()), new Rect(0, 0, w, h), new Paint()); if (mRecycleBitmaps) { original.recycle(); overlay.recycle(); } return newBitmap; } for (int x = 0; x < w; x++) { for (int y = 0; y < h; y++) { int colour1 = original.getPixel(x, y); int colour2 = newBitmap.getPixel(x, y); double[] rgb1 = new double[]{Color.red(colour1), Color.green(colour1), Color.blue(colour1)}; double[] rgb2 = new double[]{Color.red(colour2), Color.green(colour2), Color.blue(colour2)}; int[] rgb3 = new int[]{(int)rgb2[0], (int)rgb2[1], (int)rgb2[2]}; if (blendMode == BlendMode.OVERLAY) { rgb3[0] = safe(rgb1[0] > 128.0 ? (255.0 - 2.0 * (255.0 - rgb2[0]) * (255.0 - rgb1[0]) / 255.0) : ((rgb1[0] * rgb2[0] * 2.0) / 255.0)); rgb3[1] = safe(rgb1[1] > 128.0 ? (255.0 - 2.0 * (255.0 - rgb2[1]) * (255.0 - rgb1[1]) / 255.0) : ((rgb1[1] * rgb2[1] * 2.0) / 255.0)); rgb3[2] = safe(rgb1[2] > 128.0 ? (255.0 - 2.0 * (255.0 - rgb2[2]) * (255.0 - rgb1[2]) / 255.0) : ((rgb1[2] * rgb2[2] * 2.0) / 255.0)); } else if (blendMode == BlendMode.DIFFERENCE) { rgb3[0] = safe(Math.abs(rgb2[0] - rgb1[0])); rgb3[1] = safe(Math.abs(rgb2[1] - rgb1[1])); rgb3[2] = safe(Math.abs(rgb2[2] - rgb1[2])); } else if (blendMode == BlendMode.SOFTLIGHT) { rgb3[0] = safe(rgb1[0] > 128 ? 255 - ((255 - rgb1[0]) * (255 - (rgb2[0] - 128))) / 255 : (rgb1[0] * (rgb2[0] + 128)) / 255); rgb3[1] = safe(rgb1[1] > 128 ? 255 - ((255 - rgb1[1]) * (255 - (rgb2[1] - 128))) / 255 : (rgb1[1] * (rgb2[1] + 128)) / 255); rgb3[2] = safe(rgb1[2] > 128 ? 255 - ((255 - rgb1[2]) * (255 - (rgb2[2] - 128))) / 255 : (rgb1[2] * (rgb2[2] + 128)) / 255); } Paint p = new Paint(); p.setARGB(255, rgb3[0], rgb3[1], rgb3[2]); c.drawRect(x, y, x + 1, y + 1, p); } } if (mRecycleBitmaps) { original.recycle(); overlay.recycle(); } return newBitmap; } } /** * Fixes the orientation of a bitmap. Original bitmap is recycled after this method is called * @param bm The bitmap to fix * @param currentOrientation The current orientation as discripted in {@link android.media.ExifInterface} * @return The fixed bitmap */ public static Bitmap fixOrientation(Bitmap bm, int currentOrientation) { switch (currentOrientation) { case ORIENTATION_HORIZONTAL: { return flip(bm, FLIP_HORIZONTAL); } case ORIENTATION_180_ROTATE_LEFT: { return rotate(bm, -180); } case ORIENTATION_VERTICAL_FLIP: { return flip(bm, FLIP_VERTICAL); } case ORIENTATION_VERTICAL_FLIP_90_ROTATE_RIGHT: { return rotate(flip(bm, FLIP_VERTICAL), 90); } case ORIENTATION_90_ROTATE_RIGHT: { return rotate(bm, 90); } case ORIENTATION_HORIZONTAL_FLIP_90_ROTATE_RIGHT: { return rotate(flip(bm, FLIP_HORIZONTAL), 90); } case ORIENTATION_90_ROTATE_LEFT: { return rotate(bm, -90); } } return bm; } }